refinedで値が満たすべき条件を型で表現する
こんにちは、山崎です。
今回はrefinedというライブラリをご紹介します。
refinedとは
refinedはRefinement Type(篩型)をScalaで実現するためのライブラリで、既存の型に対して型レベルで満たすべき条件を指定することで、取りうる値を制限することができます。もともとはHaskellの同名のライブラリを移植したもののようです
依存ライブラリ
まずRefinedを依存ライブラリに追加します。
libraryDependencies ++= Seq( "eu.timepit" %% "refined" % "0.8.4", )
簡単な例
まず、数値を特定の範囲に制限する例を見ていきましょう。
先に必要なパッケージをimportしておきます。
import eu.timepit.refined._ import eu.timepit.refined.api.Refined import eu.timepit.refined.auto._ import eu.timepit.refined.collection._ import eu.timepit.refined.numeric._ import eu.timepit.refined.string._
//正の値 val positive: Int Refined Positive = 10 // コンパイルエラー // val positive :Int Refined Positive = -1
positiveにはInt Refined Positive
という型が指定されています。Refined
の後ろに指定する型はPredicateと呼ばれ、値が満たすべき性質を表現しています。
今回はPredicate
としてPositive
が指定されているため、右辺に正の値のリテラルを指定した場合のみimplicit conversionによってInt Refined Positive
への変換が行われ、コンパイルが通るようになります。
また、自分で上限などを指定して範囲を指定できるPredicateも用意されています。
//2.5より小さい val `lessThan2.5`: Double Refined Less[W.`2.5`.T] = 1.8 //1から3まで val inTheRange: Int Refined Interval.Closed[W.`1`.T, W.`3`.T] = 2
W.`1`.T
などとしている部分では1
というリテラルの型(Literal Singleton Type)を取り出しています。この型をLess
やClosed
のに渡してやることにより、コンパイル時に条件がチェックされるようになります。
余談ですが、このLiteral Singleton TypeについてはSIP-23で改善案が提案されており、DottyではすでにW.`3`.T
などとしなくても、
val a: 3 = 3
のようにリテラルを直接使って型を表現することが可能になっているようです。
リテラルでない値についてはコンパイル時に値の性質がわからないため、自動でRefineされた型の値に変換することはできません。
このような場合にはrefineV
を使用します。
val eitherPositive: Either[String, Positive] = refineV(userInput)
コレクションや文字列の例
数値だけでなくTraversable
や文字列に対しても同様のことを行うことができます。
//必ず一つ以上の要素を含む type NonEmptyCollection = Seq[String] Refined NonEmpty val strings = List("hoge", "fuga", "tom") val nonEmpty: Either[String, NonEmptyCollection] = refineV(strings) //正の数値を含む type ContainsPositive = Seq[Int] Refined Exists[Positive] val numbers = List(1, 3 ,5) val containsPositive: Either[String, ContainsPositive] = refineV(numbers) // Right //文字列"hoge"から始まる val startsWithHoge: String Refined StartsWith[W.`"hoge"`.T] = "hogefuga" //パターンにマッチする val matched: String Refined MatchesRegex[W.`"[a-z]0[A-Z]+"`.T] = "a0AHFOEHF" // コンパイルエラー //val matched: String Refined MatchesRegex[W.`"[a-z]0[A-Z]+"`.T] = "abcde"
また、URLや正規表現については以下のようなものが最初から用意されています。
val url: String Refined Url = "http://example.com" val regex: String Refined Regex = "[a-z]0[A-Z]+"
ここまで幾つかの条件を紹介してきましたが、他にもたくさんのPredicateが用意されており、こちらに紹介されています。
また、独自の型についてのPredicateなどを作成することももちろん可能であり、そちらに関しましてはこちらのドキュメントに方法が紹介されています。
Circeと一緒に使う
Refinedの良いところは対応しているライブラリが多いところです。CirceやPlay Json, ScalaCheckなどはすでに対応するためのモジュールが存在します。例としてCirceと一緒に使うケースを紹介します。
まず、libraryDependenciesを以下のように編集します
libraryDependencies ++= Seq( "eu.timepit" %% "refined" % "0.8.4", "io.circe" %% "circe-core" % "0.8.0", "io.circe" %% "circe-generic" % "0.8.0", "io.circe" %% "circe-parser" % "0.8.0", "io.circe" %% "circe-refined" % "0.8.0" )
CirceによってJsonからデコードするcase classを作成します。 case classのそれぞれのフィールドは、型によって満たすべき条件が表現されています。
case class Datum(positive: Int Refined Positive, startWithHoge: String Refined StartsWith[W.`"hoge"`.T])
この値に対してJsonをデコードするコードを書きます。
import eu.timepit.refined._ import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Positive import eu.timepit.refined.string.StartsWith import io.circe.parser._ import io.circe.generic.auto._ import io.circe.refined._ //Refinedの型に対応 object RefinedWithCirce extends App { val notPositiveJson = """ |{ | "positive": -1, | "startWithHoge": "startWithHoge" |} """.stripMargin //positiveの値が負なのでdecodeに失敗する println("decode[Datum](notPositiveJson) = " + decode[Datum](notPositiveJson)) val validJson = """ |{ | "positive": 2, | "startWithHoge" : "hogehoge" |} """.stripMargin // 正しいJson println("decode[Datum](validJson) = " + decode[Datum](validJson)) }
上のコードを実行すると以下のように出力されます。
decode[Datum](notPositiveJson) = Left(DecodingFailure(Predicate failed: (-1 > 0)., List(DownField(positive)))) decode[Datum](validJson) = Right(Datum(2,hogehoge))
型にInt Refined Positive
を指定したフィールドに対してJsonで負の値が入っている場合には、decode
の戻り値としてLeft
が返ってきていることがわかります。また、きちんと型で明示された条件を満たす値が入っている時はRight
が戻り値として返ってきています。
通常はロジックとして表現する条件を型として表現することで妥当性の検査を含むJsonのデコードがとても簡潔に記述できました。
まとめ
Refinedを使用することで既存の方に満たすべき条件を付加することができることをご紹介しました。また、型として表現しておくことでJsonからデシリアライズする際のバリデーションが簡潔になることもお分かりいただけたと思います。
個人的にもとても興味を惹かれるライブラリで今後も継続的にウォッチしていきたいと思いました。